package com.sardine.base.mybatis.interceptor;

import com.sardine.base.mybatis.MyBatisPageBean;
import com.sardine.base.mybatis.OrderEnum;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;

/**
 * ****************************************
 * Created by IntelliJ IDEA 14.1.12
 * Mybatis采用责任链模式，通过动态代理组织多个拦截器（插件），通过这些拦截器可以改变Mybatis的默认行为（诸如SQL重写之类的），
 * 由于插件会深入到Mybatis的核心，因此在编写自己的插件前最好了解下它的原理，以便写出安全高效的插件。
 * Mybatis支持对Executor、StatementHandler、PameterHandler和ResultSetHandler进行拦截，也就是说会对这4种对象进行代理。
 *
 * @author daif
 * @version V1.0
 * @company 北京京东世纪贸易有限公司
 * @description helloProfile
 * @date 2015/5/31 15:08
 * email:   daifeng@jd.com
 * Copyright © 2015 JD Systems Incorporated. All rights reserved.
 * *****************************************
 */
/*从签名里可以看出，要拦截的目标类型是StatementHandler（注意：type只能配置成接口类型），拦截的方法是名称为prepare参数为Connection等类型的方法。*/
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class,Integer.class })})
public class PaginationInterceptor implements Interceptor {
    private static final Logger logger = LoggerFactory.getLogger(PaginationInterceptor.class);

    private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
    private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
    private static final ReflectorFactory DEFAULT_OBJECT_REFLECTOR_FACTORY = new DefaultReflectorFactory();
    private static String defaultDialect = "mysql"; // 数据库类型(默认为mysql)
    private static String defaultPageSqlIdEndsWithPrefix = ".*Page$"; // 需要拦截的ID(正则匹配)

    private String dialect = ""; // 数据库类型(默认为mysql)
    private String pageSqlIdEndsWithPrefix = ""; // 需要拦截的ID(正则匹配)

    /**
     * 是实现拦截逻辑的地方，内部要通过invocation.proceed()显式地推进责任链前进，也就是调用下一个拦截器拦截目标方法。
     *
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY,
                DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_OBJECT_REFLECTOR_FACTORY);
        // 分离代理对象链(由于目标类可能被多个拦截器拦截，从而形成多次代理，通过下面的两次循环可以分离出最原始的的目标类)
        while (metaStatementHandler.hasGetter("h")) {
            Object object = metaStatementHandler.getValue("h");
            metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_OBJECT_REFLECTOR_FACTORY);
        }
        // 分离最后一个代理对象的目标类
        while (metaStatementHandler.hasGetter("target")) {
            Object object = metaStatementHandler.getValue("target");
            metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_OBJECT_REFLECTOR_FACTORY);
        }
        //配置拦截器全局的其他属性
        interceptorGlobalConfig(metaStatementHandler);
        MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
        // 只重写需要分页的sql语句。通过MappedStatement的ID匹配，默认重写以Page结尾的MappedStatement的sql
        BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
        if (mappedStatement.getId().matches(pageSqlIdEndsWithPrefix)) {
            Object parameterObject = boundSql.getParameterObject();
            if (parameterObject == null) {
                throw new NullPointerException("parameterObject is null!");
            } else {
                //分页对象
                MyBatisPageBean page = (MyBatisPageBean) metaStatementHandler
                        .getValue("delegate.boundSql.parameterObject.pageBean");
                //排序对象
                String sortName = (String) metaStatementHandler
                        .getValue("delegate.boundSql.parameterObject.sortName");
                //排序规则
                OrderEnum orderEnum = (OrderEnum) metaStatementHandler
                        .getValue("delegate.boundSql.parameterObject.sortOrder");
                String sql = boundSql.getSql();
                // 重写sql,生成排序，并分页的sql语句
                String pageSql = buildPageSql(sql, page, sortName, orderEnum);
                //根据句柄设置statement元数据分页，
                metaStatementHandler.setValue("delegate.boundSql.sql", pageSql);
                // 采用物理分页后，就不需要mybatis的内存分页了，所以重置下面的两个参数
                metaStatementHandler.setValue("delegate.rowBounds.offset", RowBounds.NO_ROW_OFFSET);
                metaStatementHandler.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT);
                //获取被拦截的方法参数
                Connection connection = (Connection) invocation.getArgs()[0];
                // 重设分页参数里的总页数等
                setPageParameter(sql, connection, mappedStatement, boundSql, page);
            }
        }
        // 将执行权交给下一个拦截器
        return invocation.proceed();
    }

    /**
     * 就是用当前这个拦截器生成对目标target的代理，实际是通过Plugin.wrap(target,this) 来完成的，把目标target和拦截器this传给了包装函数。
     *
     * @param target
     * @return
     */
    @Override
    public Object plugin(Object target) {
        // 当目标类是StatementHandler类型时，才包装目标类，否者直接返回目标本身,减少目标被代理的次数
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    /**
     * 用于设置额外的参数，参数配置在拦截器的Properties节点里。
     * <p/>
     * 注解里描述的是指定拦截方法的签名  [type,method,args] （即对哪种对象的哪种方法进行拦截），它在拦截前用于决断。
     *
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {
        //设置分页sql语句的id 结束符号
        this.pageSqlIdEndsWithPrefix = properties.getProperty("pageSqlIdEndsWithPrefix");
        //设置分页方言
        this.dialect = properties.getProperty("dialect");
    }

    /**
     * 从mybatis文件中读取全局属性，如果属性
     *
     * @param metaStatementHandler
     */
    private void interceptorGlobalConfig(MetaObject metaStatementHandler) {
        Configuration configuration = (Configuration) metaStatementHandler.getValue("delegate.configuration");
        //检查是否拦截器自己配置了属性
        if (null == dialect || "".equals(dialect)) {
            //没有配置就使用全局配置
            dialect = configuration.getVariables().getProperty("dialect");
        }
        //全局配置也没有，就使用默认配置
        if (null == dialect || "".equals(dialect)) {
            logger.warn("Property dialect is not setted,use default 'mysql' ");
            dialect = defaultDialect;
        }
        //分页查询ID结束标示
        if (null == pageSqlIdEndsWithPrefix || "".equals(pageSqlIdEndsWithPrefix)) {
            pageSqlIdEndsWithPrefix = configuration.getVariables().getProperty("pageSqlIdEndsWithPrefix");
        }
        if (null == pageSqlIdEndsWithPrefix || "".equals(pageSqlIdEndsWithPrefix)) {
            logger.warn("Property pageSqlIdEndsWithPrefix is not setted,use default '.*Page$' ");
            pageSqlIdEndsWithPrefix = defaultPageSqlIdEndsWithPrefix;
        }
    }

    /**
     * 从数据库里查询总的记录数并计算总页数，回写进分页参数<code>PageParameter</code>,这样调用者就可用通过 分页参数
     * <code>PageParameter</code>获得相关信息。
     *
     * @param sql
     * @param connection
     * @param mappedStatement
     * @param boundSql
     * @param page
     */
    private void setPageParameter(String sql, Connection connection, MappedStatement mappedStatement,
                                  BoundSql boundSql, MyBatisPageBean page) {
        //将字符串转换为小写字母
        String tempSQL = sql.toLowerCase();
//        StringTokenizer st = new StringTokenizer(tempSQL);
//        List<String> sqlString = new ArrayList<>();
//        while (st.hasMoreTokens()) {
//            sqlString.add(st.nextToken());
//        }
//        int i = sqlString.indexOf("from");
//        List<String> strings = sqlString.subList(i, sqlString.size());
//        String join = StringUtils.join(strings.iterator(), " ");
        //截取替换 记录总记录数,带左右空白是因为from是独立的字符，sql语句的书写规范
//        String countSql = "select count(1) " + join;
        String countSql = "select count(1) from (" + tempSQL+" ) tempSQlTable";
        PreparedStatement countStmt = null;
        ResultSet rs = null;
        try {
            countStmt = connection.prepareStatement(countSql);
            BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), countSql,
                    boundSql.getParameterMappings(), boundSql.getParameterObject());
            setParameters(countStmt, mappedStatement, countBS, boundSql.getParameterObject());
            rs = countStmt.executeQuery();
            int totalCount = 0;
            if (rs.next()) {
                totalCount = rs.getInt(1);
            }
            page.setTotalCount(totalCount);
//            int totalPage = totalCount / page.getPageSize() + ((totalCount % page.getPageSize() == 0) ? 0 : 1);
//            page.setTotalPage(totalPage);
        } catch (SQLException e) {
            logger.error("Ignore this exception", e);
        } finally {
            try {
                if (rs != null) {
                    rs.close();
                }
            } catch (SQLException e) {
                logger.error("Ignore this exception", e);
            }
            try {
                if (countStmt != null) {
                    countStmt.close();
                }
            } catch (SQLException e) {
                logger.error("Ignore this exception", e);
            }
        }
    }

    /**
     * 对SQL参数(?)设值
     *
     * @param ps
     * @param mappedStatement
     * @param boundSql
     * @param parameterObject
     * @throws SQLException
     */
    private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql,
                               Object parameterObject) throws SQLException {
        ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
        parameterHandler.setParameters(ps);
    }


    /**
     * 根据数据库类型，生成特定的分页sql
     *
     * @param sql       原始语句
     * @param page      分页对象
     * @param sortName  排序字段
     * @param orderEnum 排序字段
     * @return
     */
    private String buildPageSql(String sql, MyBatisPageBean page, String sortName, OrderEnum orderEnum) {
        //构建排序语句
        String sortSQL = orderSQL(sql, sortName, orderEnum);
        //构建分页语句并返回
        return paginationSQL(page, sortSQL);

    }

    /**
     * 生成分页SQL语句
     *
     * @param page
     * @param resultSQL
     * @return
     */
    private String paginationSQL(MyBatisPageBean page, String resultSQL) {
        //再判断分页
        if (page != null) {
            StringBuilder pageSql = new StringBuilder();
            if ("mysql".equals(dialect)) {
                pageSql = buildPageSqlForMysql(resultSQL, page);
            } else if ("oracle".equals(dialect)) {
                pageSql = buildPageSqlForOracle(resultSQL, page);
            } else {
                return resultSQL;
            }
            return pageSql.toString();
        } else {
            return resultSQL;
        }
    }

    /**
     * 生成排序Sql
     *
     * @param sql       原始sql
     * @param sortName  排序字段
     * @param orderEnum 排序字段
     * @return
     */
    private String orderSQL(final String sql, final String sortName, final OrderEnum orderEnum) {
        StringBuilder resultSQL = new StringBuilder(100);
        resultSQL.append(sql);
        if (sortName != null && sortName.trim().length() > 0) {
            resultSQL.append(" ORDER BY ");
            resultSQL.append(sortName);
            resultSQL.append(orderEnum == OrderEnum.ASC ? " asc" : " desc");
        }
        return resultSQL.toString();
    }

    /**
     * mysql的分页语句
     *
     * @param sql
     * @param page
     * @return String
     */
    protected StringBuilder buildPageSqlForMysql(String sql, MyBatisPageBean page) {
        StringBuilder pageSql = new StringBuilder(100);
        String beginrow = String.valueOf((page.getPageNo() - 1) * page.getPageSize());
        pageSql.append(sql);
        pageSql.append(" limit " + beginrow + "," + page.getPageSize());
        return pageSql;
    }

    /**
     * 参考hibernate的实现完成oracle的分页,暂时未使用到
     *
     * @param sql
     * @param page
     * @return String
     */
    protected StringBuilder buildPageSqlForOracle(String sql, MyBatisPageBean page) {
        StringBuilder pageSql = new StringBuilder(100);
        String beginrow = String.valueOf((page.getPageNo() - 1) * page.getPageSize());
        String endrow = String.valueOf(page.getPageNo() * page.getPageSize());

        pageSql.append("select * from ( select temp.*, rownum row_id from ( ");
        pageSql.append(sql);
        pageSql.append(" ) temp where rownum <= ").append(endrow);
        pageSql.append(") where row_id > ").append(beginrow);
        return pageSql;
    }
}
